/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2008  David Koelle
 *
 * http://www.jfugue.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

package org.jfugue;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.swing.event.EventListenerList;

/**
 * This class represents a segment of music. By representing segments of music
 * as patterns, JFugue gives users the opportunity to play around with pieces of
 * music in new and interesting ways. Patterns may be added together,
 * transformed, or otherwise manipulated to expand the possibilities of creative
 * music.
 *
 * @author David Koelle
 * @version 2.0
 * @version 4.0 - Added Pattern Properties
 * @version 4.0.3 - Now implements Serializable
 */
public class Pattern {
	private StringBuilder musicString;
	private Map<String, String> properties;

	/**
	 * Instantiates a new pattern
	 */
	public Pattern() {
		this("");
	}

	/**
	 * Instantiates a new pattern using the given music string
	 * 
	 * @param musicString
	 *            the music string
	 */
	public Pattern(String musicString) {
		setMusicString(musicString);
		properties = new HashMap<String, String>();
	}

	/** Copy constructor */
	public Pattern(Pattern pattern) {
		this(pattern.getMusicString());
		Iterator<String> iter = pattern.getProperties().keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			String value = pattern.getProperty(key);
			setProperty(key, value);
		}
	}

	/**
	 * This constructor creates a new Pattern that contains each of the given
	 * patterns
	 * 
	 * @version 4.0
	 */
	public Pattern(Pattern... patterns) {
		this();
		for (Pattern p : patterns) {
			this.add(p);
		}
	}

	/**
	 * Creates a Pattern given a MIDI file - do not use. Note the Package scope,
	 * limiting this method to be called only by JFugue. If you want to load
	 * MIDI, use Player.loadMidi, which sets the sequence timing correctly.
	 * 
	 * @param file
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 */
	static Pattern loadMidi(File file)
			throws IOException, InvalidMidiDataException {
		MidiParser parser = new MidiParser();
		MusicStringRenderer renderer = new MusicStringRenderer();
		parser.addParserListener(renderer);
		parser.parse(MidiSystem.getSequence(file));
		Pattern pattern = new Pattern(renderer.getPattern().getMusicString());
		return pattern;
	}

	/**
	 * Sets the music string kept by this pattern.
	 * 
	 * @param musicString
	 *            the music string
	 */
	public void setMusicString(String musicString) {
		this.musicString = new StringBuilder();
		this.musicString.append(musicString);
	}

	/**
	 * Adds to the music string kept by this pattern.
	 * 
	 * @param appendString
	 *            the music string to add
	 */
	private void appendMusicString(String appendString) {
		this.musicString.append(appendString);
	}

	/**
	 * Returns the music string kept in this pattern
	 * 
	 * @return the music string
	 */
	public String getMusicString() {
		return this.musicString.toString();
	}

	/**
	 * Inserts a MusicString before this music string. NOTE - this does not call
	 * fragmentAdded!
	 * 
	 * @param musicString
	 *            the string to insert
	 */
	public void insert(String musicString) {
		this.musicString.insert(0, " ");
		this.musicString.insert(0, musicString);
	}

	/**
	 * Adds an additional pattern to the end of this pattern.
	 * 
	 * @param pattern
	 *            the pattern to add
	 */
	public void add(Pattern pattern) {
		fireFragmentAdded(pattern);
		appendMusicString(" ");
		appendMusicString(pattern.getMusicString());
	}

	/**
	 * Adds a music string to the end of this pattern.
	 * 
	 * @param musicString
	 *            the music string to add
	 */
	public void add(String musicString) {
		add(new Pattern(musicString));
	}

	/**
	 * Adds an additional pattern to the end of this pattern.
	 * 
	 * @param pattern
	 *            the pattern to add
	 */
	public void add(Pattern pattern, int numTimes) {
		for (int i = 0; i < numTimes; i++) {
			fireFragmentAdded(pattern);
			appendMusicString(" ");
			appendMusicString(pattern.getMusicString());
		}
	}

	/**
	 * Adds a music string to the end of this pattern.
	 * 
	 * @param musicString
	 *            the music string to add
	 */
	public void add(String musicString, int numTimes) {
		add(new Pattern(musicString), numTimes);
	}

	/**
	 * Adds a number of patterns sequentially
	 * 
	 * @param patterns
	 *            the music string to add
	 * @version 4.0
	 */
	public void add(Pattern... patterns) {
		for (Pattern pattern : patterns) {
			add(pattern);
		}
	}

	/**
	 * Adds a number of patterns sequentially
	 * 
	 * @param musicStrings
	 *            the music string to add
	 * @version 4.0
	 */
	public void add(String... musicStrings) {
		for (String string : musicStrings) {
			add(string);
		}
	}

	/**
	 * Adds an individual element to the pattern. This takes into account the
	 * possibility that the element may be a sequential or parallel note, in
	 * which case no space is placed before it.
	 * 
	 * @param element
	 *            the element to add
	 */
	public void addElement(JFugueElement element) {
		String elementMusicString = element.getMusicString();

		// Don't automatically add a space if this is a continuing note event
		if ((elementMusicString.charAt(0) == '+')
				|| (elementMusicString.charAt(0) == '_')) {
			appendMusicString(elementMusicString);
		} else {
			appendMusicString(" ");
			appendMusicString(elementMusicString);
			fireFragmentAdded(new Pattern(elementMusicString));
		}
	}

	/**
	 * Sets the title for this Pattern. As of JFugue 4.0, the title is set as a
	 * property with the key Pattern.TITLE
	 * 
	 * @param title
	 *            the title for this Pattern
	 */
	public void setTitle(String title) {
		setProperty(TITLE, title);
	}

	/**
	 * Returns the title of this Pattern As of JFugue 4.0, the title is set as a
	 * property with the key Pattern.TITLE
	 * 
	 * @return the title of this Pattern
	 */
	public String getTitle() {
		return getProperty(TITLE);
	}

	/**
	 * Get a property on this pattern, such as "author" or "date".
	 * 
	 * @version 4.0
	 */
	public String getProperty(String key) {
		return properties.get(key);
	}

	/**
	 * Set a property on this pattern, such as "author" or "date".
	 * 
	 * @version 4.0
	 */
	public void setProperty(String key, String value) {
		properties.put(key, value);
	}

	/**
	 * Get all properties set on this pattern, such as "author" or "date".
	 * 
	 * @version 4.0
	 */
	public Map<String, String> getProperties() {
		return properties;
	}

	/**
	 * Repeats the music string in this pattern by the given number of times.
	 * Example: If the pattern is "A B", calling <code>repeat(4)</code> will
	 * make the pattern "A B A B A B A B".
	 * 
	 * @version 3.0
	 */
	public void repeat(int times) {
		repeat(null, getMusicString(), times, null);
	}

	/**
	 * Only repeats the portion of this music string that starts at the string
	 * index provided. This allows some initial header information to only be
	 * specified once in a repeated pattern. Example: If the pattern is "T0 A B"
	 * , calling <code>repeat(4, 3)</code> will make the pattern
	 * "T0 A B A B A B A B".
	 * 
	 * @version 3.0
	 */
	public void repeat(int times, int beginIndex) {
		String string = getMusicString();
		repeat(string.substring(0, beginIndex), string.substring(beginIndex),
				times, null);
	}

	/**
	 * Only repeats the portion of this music string that starts and ends at the
	 * string indices provided. This allows some initial header information and
	 * trailing information to only be specified once in a repeated pattern.
	 * Example: If the pattern is "T0 A B C", calling
	 * <code>repeat(4, 3, 5)</code> will make the pattern "T0 A B A B A B A B C"
	 * .
	 * 
	 * @version 3.0
	 */
	public void repeat(int times, int beginIndex, int endIndex) {
		String string = getMusicString();
		repeat(string.substring(0, beginIndex),
				string.substring(beginIndex, endIndex), times,
				string.substring(endIndex));
	}

	private void repeat(String header, String repeater, int times,
			String trailer) {
		StringBuffer buffy = new StringBuffer();

		// Add the header, if it exists
		if (header != null) {
			buffy.append(header);
		}

		// Repeat and add the repeater
		for (int i = 0; i < times; i++) {
			buffy.append(repeater);
			if (i < times - 1) {
				buffy.append(" ");
			}
		}

		// Add the trailer, if it exists
		if (trailer != null) {
			buffy.append(trailer);
		}

		// Create the new Pattern and return it
		this.setMusicString(buffy.toString());
	}

	/**
	 * Returns a new Pattern that is a subpattern of this pattern.
	 * 
	 * @return subpattern of this pattern
	 * @version 3.0
	 */
	public Pattern subPattern(int beginIndex) {
		return new Pattern(substring(beginIndex));
	}

	/**
	 * Returns a new Pattern that is a subpattern of this pattern.
	 * 
	 * @return subpattern of this pattern
	 * @version 3.0
	 */
	public Pattern subPattern(int beginIndex, int endIndex) {
		return new Pattern(substring(beginIndex, endIndex));
	}

	protected String substring(int beginIndex) {
		return getMusicString().substring(beginIndex);
	}

	protected String substring(int beginIndex, int endIndex) {
		return getMusicString().substring(beginIndex, endIndex);
	}

	public static Pattern loadPattern(File file) throws IOException {
		StringBuffer buffy = new StringBuffer();

		Pattern pattern = new Pattern();

		BufferedReader bread = new BufferedReader(
				new InputStreamReader(new FileInputStream(file)));
		while (bread.ready()) {
			String s = bread.readLine();
			if ((s != null) && (s.length() > 1)) {
				if (s.charAt(0) != '#') {
					buffy.append(" ");
					buffy.append(s);
				} else {
					String key = s.substring(1, s.indexOf(':')).trim();
					String value = s.substring(s.indexOf(':') + 1, s.length())
							.trim();
					if (key.equalsIgnoreCase(TITLE)) {
						pattern.setTitle(value);
					} else {
						pattern.setProperty(key, value);
					}
				}
			}
		}
		bread.close();
		pattern.setMusicString(buffy.toString());

		return pattern;
	}

	/**
	 * Saves the pattern as a text file
	 * 
	 * @param file
	 *            the filename to save under
	 */
	public void savePattern(File file) throws IOException {
		BufferedWriter out = new BufferedWriter(
				new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8));
		if ((getProperties().size() > 0) || (getTitle() != null)) {
			out.write("#\n");
			if (getTitle() != null) {
				out.write("# ");
				out.write("Title: ");
				out.write(getTitle());
				out.write("\n");
			}
			Iterator<String> iter = getProperties().keySet().iterator();
			while (iter.hasNext()) {
				String key = iter.next();
				if (!key.equals(TITLE)) {
					String value = getProperty(key);
					out.write("# ");
					out.write(key);
					out.write(": ");
					out.write(value);
					out.write("\n");
				}
			}
			out.write("#\n");
			out.write("\n");
		}
		String musicString = getMusicString();
		while (musicString.length() > 0) {
			if ((musicString.length() > 80)
					&& (musicString.indexOf(' ', 80) > -1)) {
				int indexOf80ColumnSpace = musicString.indexOf(' ', 80);
				out.write(musicString.substring(0, indexOf80ColumnSpace));
				out.newLine();
				musicString = musicString.substring(indexOf80ColumnSpace,
						musicString.length());
			} else {
				out.write(musicString);
				musicString = "";
			}
		}
		out.close();
	}

	/**
	 * Returns a String containing key-value pairs stored in this object's
	 * properties, separated by semicolons and spaces. Values are returned in
	 * the following form: key1: value1; key2: value2; key3: value3
	 *
	 * @return a String containing key-value pairs stored in this object's
	 *         properties, separated by semicolons and spaces
	 */
	public String getPropertiesAsSentence() {
		StringBuilder buddy = new StringBuilder();
		Iterator<String> iter = getProperties().keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			String value = getProperty(key);
			buddy.append(key);
			buddy.append(": ");
			buddy.append(value);
			buddy.append("; ");
		}
		String result = buddy.toString();
		return result.substring(0, result.length() - 2); // Take off the last
															// semicolon-space
	}

	/**
	 * Returns a String containing key-value pairs stored in this object's
	 * properties, separated by newline characters.
	 *
	 * Values are returned in the following form: key1: value1\n key2: value2\n
	 * key3: value3\n
	 *
	 * @return a String containing key-value pairs stored in this object's
	 *         properties, separated by newline characters
	 */
	public String getPropertiesAsParagraph() {
		StringBuilder buddy = new StringBuilder();
		Iterator<String> iter = getProperties().keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			String value = getProperty(key);
			buddy.append(key);
			buddy.append(": ");
			buddy.append(value);
			buddy.append("\n");
		}
		String result = buddy.toString();
		return result.substring(0, result.length());
	}

	/**
	 * Changes all timestamp values by the offsetTime passed in. NOTE: This
	 * method is only useful for patterns that have been converted from a MIDI
	 * file.
	 * 
	 * @param offsetTime
	 */
	public void offset(long offsetTime) {
		StringBuffer buffy = new StringBuffer();
		String[] tokens = getMusicString().split(" ");
		for (int i = 0; i < tokens.length; i++) {
			if ((tokens[i].length() > 0) && (tokens[i].charAt(0) == '@')) {
				String timeNumberString = tokens[i].substring(1,
						tokens[i].length());
				if (timeNumberString.indexOf("[") == -1) {
					long timeNumber = Long.parseLong(timeNumberString);
					long newTime = timeNumber + offsetTime;
					if (newTime < 0) {
						newTime = 0;
					}
					buffy.append("@" + newTime);
				} else {
					buffy.append(tokens[i]);
				}
			} else {
				buffy.append(tokens[i]);
			}
			buffy.append(" ");
		}
		setMusicString(buffy.toString());
	}

	/**
	 * Returns an array of strings representing each token in the Pattern.
	 * 
	 * @return
	 */
	public String[] getTokens() {
		StringTokenizer strtok = new StringTokenizer(musicString.toString(),
				" \n\t");

		List<String> list = new ArrayList<String>();
		while (strtok.hasMoreTokens()) {
			String token = strtok.nextToken();
			if (token != null) {
				list.add(token);
			}
		}

		String[] retVal = new String[list.size()];
		list.toArray(retVal);
		return retVal;
	}

	/**
	 * Indicates whether the provided musicString is composed of valid elements
	 * that can be parsed by the Parser.
	 * 
	 * @param musicString
	 *            the musicString to test
	 * @return whether the musicString is valid
	 * @version 3.0
	 */
	// public static boolean isValidMusicString(String musicString)
	// {
	// try {
	// Parser parser = new Parser();
	// parser.parse(musicString);
	// } catch (JFugueException e)
	// {
	// return false;
	// }
	// return true;
	// }

	//
	// Listeners
	//

	/** List of ParserListeners */
	protected EventListenerList listenerList = new EventListenerList();

	/**
	 * Adds a <code>PatternListener</code>. The listener will receive events
	 * when new parts are added to the pattern.
	 *
	 * @param listener
	 *            the listener that is to be notified when new parts are added
	 *            to the pattern
	 */
	public void addPatternListener(PatternListener listener) {
		listenerList.add(PatternListener.class, listener);
	}

	/**
	 * Removes a <code>PatternListener</code>.
	 *
	 * @param listener
	 *            the listener to remove
	 */
	public void removePatternListener(PatternListener listener) {
		listenerList.remove(PatternListener.class, listener);
	}

	protected void clearPatternListeners() {
		EventListener[] l = listenerList.getListeners(PatternListener.class);
		int numListeners = l.length;
		for (int i = 0; i < numListeners; i++) {
			listenerList.remove(PatternListener.class, (PatternListener) l[i]);
		}
	}

	/** Tells all PatternListener interfaces that a fragment has been added. */
	private void fireFragmentAdded(Pattern fragment) {
		Object[] listeners = listenerList.getListenerList();
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == PatternListener.class) {
				((PatternListener) listeners[i + 1]).fragmentAdded(fragment);
			}
		}
	}

	/**
	 * @version 3.0
	 */
	@Override
	public String toString() {
		return getMusicString();
	}

	public static final String TITLE = "Title";
}
